package org.bhf.security.authorization;

import java.io.Serializable;
import java.util.Arrays;
import java.util.StringTokenizer;

/**
 * This delegate provides support for <code>Permission</code> classes that represent
 * operations on a resource hierarchy.
 */
public final class HierarchicalPermisionDelegate implements Serializable
{
    private static final long serialVersionUID = 7930732926638008763L;

    //
    // Class Fields
    //

    // constants used by init(int mask)
    public static final String ALL              = "<<ALL>>";
    public static final String RECURSIVE        = "-";
    public static final String WILD             = "*";
    public static final char   NEGATE           = '!';

    private static final String SEP_RECURSIVE    = "/" + RECURSIVE;
    private static final String SEP_WILD         = "/" + WILD;

    //
    // Object Fields
    //

    // is this permission for granting or denying
    private final boolean   deny;
    // does path indicate a directory? (wildcard or recursive)
    private final boolean   directory;
    // is it a recursive directory specification?
    private final boolean   recursive;
    // canonicalized dir path. In the case of
    // directories, it is the name "/blah/*" or "/blah/-" without
    // the last character (the "*" or "-").
    private final String    cpath;
    // Action strings and their hashcodes (for implies() comparison speed)
    private final String[]  actions;
    private final int[]     actionHashCodes;

    /**
     * Construct a <code>HierarchicalPermisionDelegate</code> for the given path and action
     * list.
     * @param path Resource path - '/' delimited. Must not be <code>null</code>.
     * @param actions ',' delimited action list. Must not be <code>null</code>.
     */
    public HierarchicalPermisionDelegate( String path, final String actions )
    {
        // Actions
        if( actions == null || actions.length() == 0 )
        {
            this.actions            = null;
            this.actionHashCodes    = null;
        }
        else if( actions.indexOf( ',' ) == -1 ) // Fast case for query Permissions
        {
            this.actions            = new String[] { actions };
            this.actionHashCodes    = new int[] { actions.hashCode() };
        }
        else
        {
            final StringTokenizer tokens = new StringTokenizer( actions, "," );

            this.actions            = new String[ tokens.countTokens() ];
            this.actionHashCodes    = new int[ tokens.countTokens() ];
            for( int i = 0; tokens.hasMoreTokens(); i++ )
            {
                this.actions[ i ]           = tokens.nextToken().trim();
            }
            Arrays.sort( this.actions ); // This make them canonical
            for( int i = 0; i < this.actions.length; i++ )
            {
                this.actionHashCodes[ i ]= this.actions[ i ].hashCode();
            }
        }

        // Is this a grant or deny
        if( path.length() > 0 && path.charAt( 0 ) == NEGATE )
        {
            deny    = true;
            path    = path.substring( 1 );
        }
        else
        {
            deny    = false;
        }

        // Paths
        if( path.equals( "<<ALL>>" ) )
        {
            directory   = true;
            recursive   = true;
            cpath       = "";
        }
        else if( path.endsWith( SEP_RECURSIVE ) || path.equals( RECURSIVE ) )
        {
            directory   = true;
            recursive   = true;
            cpath       = path.substring( 0, path.length() - 1 );
        }
        else if( path.endsWith( SEP_WILD ) || path.equals( WILD ) )
        {
            directory   = true;
            recursive   = false;
            cpath       = path.substring( 0, path.length() - 1 );
        }
        else
        {
            directory   = false;
            recursive   = false;
            cpath       = path;
        }
    }

    /**
     * Return <code>true</code> if this permission is recursive.
     * @return <code>true</code> if this permission is recursive.
     */
    public boolean  isRecursive()
    {
        return recursive;
    }

    /**
     * Return <code>true</code> if this permission specifies a 'directory'.
     * @return <code>true</code> if this permission specifies a 'directory'.
     */
    public boolean  isDirectory()
    {
        return directory;
    }

    /**
     * Returns <code>true</code> if this permission is a deny rather than a grant.
     * @return <code>true</code> if this permission is a deny rather than a grant.
     */
    public boolean  isDeny()
    {
        return deny;
    }

    /**
     * Return the canonical path. (Path w/o '!', '*', or '-'.)
     * @return The canonical path. (Path w/o '!', '*', or '-'.)
     */
    public String   getCanonicalPath()
    {
        return cpath;
    }

    /**
     * Checks if this <code>HierarchicalPermisionDelegate</code> object "implies" the given
     * <code>HierarchicalPermisionDelegate</code>.
     * <p>
     * More specifically, this method returns true if:<p>
     * <ul>
     * <li> <code>that</code>'s actions are a proper subset of this object's actions, and </li>
     * <li> <code>that</code>'s path is implied by this object's
     *      path. For example, "/tmp/*" implies "/tmp/foo", since
     *      "/tmp/*" encompasses the "/tmp" directory and all files in that
     *      directory, including the one named "foo".</li>
     * </ul>
     * Note that implication does not depend on whether or not the permission is a deny or grant permission.
     * @param that the <code>HierarchicalPermisionDelegate</code> to check against. Must not be <code>null</code>.
     *
     * @return <code>true</code> if the specified <code>HierarchicalPermisionDelegate</code> is implied by this object,
     *      <code>false</code> if not.
     */
    public boolean implies( final HierarchicalPermisionDelegate that )
    {
        // Check path
        final boolean pathImpies;
        if( this.directory )
        {
            if( this.recursive )
            {
                // make sure that.path is longer then path so
                // something like /foo/- does not imply /foo
                if( that.directory )
                {
                    pathImpies = (that.cpath.length() >= this.cpath.length()) && that.cpath.startsWith( this.cpath );
                }
                else
                {
                    pathImpies = ((that.cpath.length() > this.cpath.length()) && that.cpath.startsWith( this.cpath ));
                }
            }
            else
            {
                if( that.directory )
                {
                    // if the permission passed in is a directory
                    // specification, make sure that a non-recursive
                    // permission (i.e., this object) can't imply a recursive
                    // permission.
                    pathImpies = !that.recursive && this.cpath.equals( that.cpath );
                }
                else
                {
                    int last = that.cpath.lastIndexOf( '/' );
                    if( last == -1 )
                    {
                        pathImpies = false;
                    }
                    else
                    {
                        String base = that.cpath.substring( 0, last + 1 );
                        pathImpies = this.cpath.equals( base );
                    }
                }
            }
        }
        else
        {
            pathImpies = this.cpath.equals( that.cpath );
        }

        // Check actions
        boolean implies = false;
        if( pathImpies )
        {
            // Each action must be implied by one of our actions
            if( this.actions == null && that.actions == null )
            {
                implies = true;
            }
            else if( this.actions == null && that.actions != null )
            {
                implies = false;
            }
            else if( this.actions != null && that.actions == null )
            {
                implies = false;
            }
            else if( this.actions != null )
            {
                implies = true;
                for( int thatI = 0; implies && thatI < that.actions.length; thatI++ )
                {
                    final String    thatAction          = that.actions[ thatI ];
                    final int       thatActionHashCode  = thatAction.hashCode();

                    implies = false;
                    for( int thisI = 0; !implies && thisI < this.actions.length; thisI++ )
                    {
                        implies =
                            thatActionHashCode == this.actionHashCodes[ thisI ] &&
                            thatAction.equals( this.actions[ thisI ] );
                    }
                }
            }
        }

        return implies;
    }

    /**
     * Checks two <code>HierarchicalPermisionDelegate</code> objects for equality. Checks that <i>obj</i> is
     * a <code>HierarchicalPermisionDelegate</code>, and has the same path and actions as this object.
     * <p>
     * @param obj the object we are testing for equality with this object.
     * @return true if obj is a <code>HierarchicalPermisionDelegate</code>, and has the same path and
     *  actions as this <code>HierarchicalPermisionDelegate</code> object.
     */
    public boolean equals( Object obj )
    {
        if( obj == this )
            return true;

        if( !(obj instanceof HierarchicalPermisionDelegate) )
            return false;

        HierarchicalPermisionDelegate that = (HierarchicalPermisionDelegate)obj;

        return
            Arrays.equals( this.actions, that.actions ) &&
            this.deny == that.deny &&
            this.cpath.equals( that.cpath ) &&
            this.directory == that.directory &&
            this.recursive == that.recursive;
    }

    /**
     * Returns the hash code value for this object.
     *
     * @return a hash code value for this object.
     */
    public int      hashCode()
    {
        return cpath.hashCode();
    }

    /**
     * Returns the "canonical string representation" of the actions.
     * That is, this method always returns present actions in ',' delimited
     * alphabetical order.
     *
     * @return the canonical string representation of the actions.
     */
    public String   getActions()
    {
        final StringBuffer  s = new StringBuffer();

        for( int i = 0; actions != null && i < actions.length; i++ )
        {
            if( i > 0 )
                s.append( ',' );
            s.append( actions[i] );
        }

        return s.toString();
    }
}



